/*
 * mobilebone.js
 * by zhangxinxu(.com) 2014-09-26
 * https://github.com/zhangxinxu/mobilebone
 * bone of switch for mobile web app 
**/

(function(root, factory) {
    // Set up Mobilebone appropriately for the environment.
    if (typeof define === 'function' && (define.amd || define.cmd)) {
        define('mobilebone', function(exports) {
            return factory(root, exports);
        });
    // Finally, as a browser global.
    } else {
        root.Mobilebone = factory(root, {});
    }
})(this, function(root, Mobilebone) {
    // Avoid repeated callbacks
    var store = {};
    
    // Create local references to array methods we'll want to use later.
    var array = [];
    var slice = array.slice;
    
    // Is it a simple selector, from jQuery
    var isSimple = /^.[^:#\[\.,]*$/
    
    // Is it suppory history API
    var supportHistory = "pushState" in history && "replaceState" in history;
        
    Mobilebone.support = supportHistory;

    var hasInited = false;
    
    /**
     * Current version of the library. Keep in sync with `package.json`.
     *
     * @type string
    **/
    Mobilebone.VERSION = '2.3.1';
    
    /**
     * Whether catch attribute of href from element with tag 'a'
     * If the value set to false, jump links in a refresh form(not slide)
     * In most cases, you do not need to care about this parameter. 
       Except some special pages that should refresh all links, as test/index.html show.
       However, if your only want several links refesh, you can use data-ajax="false" or data-rel="external"
     *
     * @type boolean
    **/
    Mobilebone.captureLink = true;
    
    /**
     * Whether catch events of 'submit' from <form> element
     * If the value set to false, <form> is a normal form except data-ajax="true"
     * If the value set to true, <form> will submit as a ajax request, 
       and the return value will be used to create a new page and transition into meanwhile.
       However, if data-ajax="false", <form> won't submit as a ajax.
     *
     * @type boolean
    **/
    Mobilebone.captureForm = true;
    
    /**
     * The root of transition-callback
     * Default value is 'root', you can consider as window-object. 
       However, there are may many callbacks, it's impossible that all functions are global function.
       We may custom a global object to store our callbacks, such as:
       Callback = {
         fun1: function() {}, 
         fun2: function() {}, 
         fun3: function() {},  
       }
       In this case, the value of 'obilebone.rootTransition' should set Callback;
     *
     * @type object
    **/
    Mobilebone.rootTransition = root;
    
    /**
     * Whether merge(vs cover) global callback and local callback
     *
     * @type boolean
    **/
    Mobilebone.mergeCallback = true;

    /**
     *  className of animation
     *
     * @type string
    **/
    Mobilebone.classAnimation = "slide";
    /**
     *  for mark page element
     *
     * @type string
    **/
    Mobilebone.classPage = "page";
    /**
     * className for mark mask element
     *
     * @type string
    **/
    Mobilebone.classMask = "mask";
    /**
     * Whether url changes when history changes
     * If this value is false, the url will be no change.
     *
     * @type boolean
    **/
    Mobilebone.pushStateEnabled = true;
    
    if (// When running inside a FF iframe, calling replaceState causes an error. So set 'pushStateEnabled = false' 
        (window.navigator.userAgent.indexOf( "Firefox" ) >= 0 && window.top !== window)
    ) {
        Mobilebone.pushStateEnabled = false;
    }
    
    /**
     * if browser do not support history/classList, stop here
    **/
    if (supportHistory == false) return Mobilebone;
    
    /**
     * don't excute window.onpopstate when page load
    **/
    history.popstate = false;
    
    /**
     * Function for transition
     * In most cases, you are unnecessary to use this function , unlike Mobilebone.createPage
     
     * @params  pageInto: dom-object. Element which will transform into. - Necessary
                pageOut:  dom-object. Element which will transform out.   - Optional
                back:     boolean.    Direction of transition.          - Optional
                options:  object.     Cover or add parameters.           - Optional
     * @returns undefined
     * @example Mobilebone.transition(element);
                Mobilebone.transition(element1, element2);
                Mobilebone.transition(element1, element2, true);
                Mobilebone.transition(element1, element2, { id: "only" });
                Mobilebone.transition(element1, element2, true, { id: "only" });
    **/
    Mobilebone.transition = function(pageInto, pageOut, back, options) {
        if (arguments.length == 0 || pageInto == pageOut) return;
        if (arguments.length == 3 && isNaN(back * 1) == true) {
            options = back;
            back = false;
        };
        //if those parameters is missing
        pageOut = pageOut || null, back = back || false, options = options || {};
        
        // defaults parameters
        var defaults = {
            // the value of callback is a key name, and the host is root here. 
            // eg. if the name of animationstart is 'doLoading', so the script will execute 'root.doLoading()'
            // By default, the value of root is 'window'
            root: this.rootTransition,
            // the form of transition, the value (eg. 'slide') will be a className to add or remove. 
            // of course, u can set to other valeu, for example, 'fade' or 'flip'. However, u shou add corresponding CSS3 code.
            form: this.form || this.classAnimation,
            // 'animationstart/animationend/...' are callbacks params
            // Note: those all global callbacks!
            onpagefirstinto: this.onpagefirstinto,
            animationstart: this.animationstart,
            animationend: this.animationend,
            preventdefault: this.preventdefault,
            fallback: this.fallback,
            callback: this.callback
        }, params = function(element) {
            if (!element || !element.getAttribute) return {};
            
            var _params = {}, _dataparams = _queryToObject(element.getAttribute("data-params") || '');
            
            // rules as follow:
            // data-* > data-params > options > defaults    
            ["title", "root", "form"].forEach(function(key) {   
                _params[key] = element.getAttribute("data-" + key) || _dataparams[key] || options[key] || defaults[key];
            });
            
            if (typeof _params.root == "string") {
                _params.root = Mobilebone.getFunction(_params.root);
            }
            
            ["onpagefirstinto", "callback", "fallback", "animationstart", "animationend", "preventdefault"].forEach(function(key) {
                if (Mobilebone.mergeCallback == true && typeof defaults[key] == "function") {
                    // merge global callback
                    var local_function_key = element.getAttribute("data-" + key) || _dataparams[key];
                    if (typeof _params.root[local_function_key] == "function") {        
                        _params[key] = function() {
                            defaults[key].apply(this, arguments);
                            _params.root[local_function_key].apply(this, arguments);
                        }
                    } else if (typeof options[key] == "function") {
                        _params[key] = function() {
                            defaults[key].apply(this, arguments);
                            options[key].apply(this, arguments);
                        }
                    } else {
                        _params[key] = defaults[key];
                    }
                } else {
                    // replace global callback
                    _params[key] = element.getAttribute("data-" + key) || _dataparams[key] || options[key] || defaults[key];
                }
            });
            
            return _params;
        };
        
        // get params of each 
        var params_out = params(pageOut), params_in = params(pageInto);
        
        if (pageOut != null && pageOut.classList) {
            // weather prevent transition
            var preventOut = params_out.preventdefault, isPreventOut = false;
            if (typeof preventOut == "string") preventOut = params_out.root[preventOut];
        }
        if (pageInto != null && pageInto.classList) {
            // weather prevent transition
            var preventInto = params_in.preventdefault, isPreventInto = false;
            if (typeof preventInto == "string") preventInto = params_in.root[preventInto];
            
        }
        if (typeof preventOut == "function") isPreventOut = preventOut.call(params_out.root, pageInto, pageOut, options);
        
        // if functions of 'preventdefault' are same for pageIn and pageout, just excute once.
        if (isPreventOut == true && preventOut === preventInto) return false;
        
        if (typeof preventInto == "function") isPreventInto = preventInto.call(params_in.root, pageInto, pageOut, options);
        // if pageinto stopped, stop all
        if (isPreventInto == true) {
            // only run here and nothing more
            return false;   
        }
        
        if (pageOut != null && pageOut.classList) {
            // do transition if there are no 'prevent'
            if (isPreventOut != true) {
                pageOut.style.display = "block";
                pageOut.classList.add("out");
                pageOut.classList.add(params_out.form);
                pageOut.classList.remove("in");
                // if reverse direction
                pageOut.classList[back? "add": "remove"]("reverse");
                // do fallback every time
                var fallback = params_out.fallback;
                if (typeof fallback == "string") fallback = params_out.root[fallback];
                if (typeof fallback == "function") fallback.call(params_out.root, pageInto, pageOut, options);
            }
        }
        if (pageInto != null && pageInto.classList) {               
            // for title change
            var title = params_in.title, 
                header = document.querySelector("h1"), 
                first_page = document.querySelector("." + this.classPage);      
            // do title change  
            if (title) {
                document.title = title;
                if (header) {
                    header.innerHTML = title;
                    header.title = title;
                }
            } else if (first_page == pageInto && !pageOut && document.title) {
                // set data-title for first visibie page
                pageInto.setAttribute("data-title", document.title);
            }
            
            // delete page with same id when options.remove !== false
            var pageid = options.id || pageInto.id;

            if (options.remove !== false && store[pageid] && store[pageid] != pageInto && store[pageid].parentElement) {
                store[pageid].parentElement.removeChild(store[pageid]);
                delete store[pageid];
            }
    
            // do transition
            pageInto.style.display = "block";
            pageInto.classList.remove("out");
            pageInto.classList.add("in");
            pageOut && pageInto.classList.add(params_in.form);
            // if reverse direction
            pageInto.classList[back? "add": "remove"]("reverse");
            
            // do callback when come in first time
            var onpagefirstinto = params_in.onpagefirstinto;
            if (!store[pageid]) {
                if (typeof onpagefirstinto == "string" && params_in.root[onpagefirstinto]) {
                    params_in.root[onpagefirstinto](pageInto, pageOut, options);
                } else if (typeof onpagefirstinto == "function") {
                    onpagefirstinto(pageInto, pageOut, options);
                }
                // capture form submit
                slice.call(pageInto.querySelectorAll("form")).forEach(function(form) {
                    Mobilebone.submit(form);
                });     
            }
            
            // do callback when animation start/end
            var isWebkit = 'WebkitAppearance' in document.documentElement.style || typeof document.webkitHidden != "undefined";
            ["animationstart", "animationend"].forEach(function(animationkey, index) {
                var animition = params_in[animationkey], webkitkey = "webkit" + animationkey.replace(/^a|s|e/g, function(matchs) {
                    return matchs.toUpperCase();
                });
                if (!store[pageid]) {
                    var animateEventName = isWebkit? webkitkey: animationkey;
                    // if it's the out element, hide it when 'animationend'
                    index && pageInto.addEventListener(animateEventName, function() {
                        if (this.classList.contains("in") == false) {
                            this.style.display = "none";
                        }
                        this.classList.remove(params(this).form);
                    });
                    // bind animation events
                    if (typeof animition == "string" && params_in.root[animition]) {
                        pageInto.addEventListener(animateEventName, function() {
                            params_in.root[animition](this, this.classList.contains("in")? "into": "out", options);
                        });
                    } else if (typeof animition == "function") {
                        pageInto.addEventListener(animateEventName, function() {
                            animition(this, this.classList.contains("in")? "into": "out", options); 
                        });
                    }   
                } 
            });
            
            // history
            var url_push = pageid;
            if (url_push && /^#/.test(url_push) == false) {
                url_push = "#" + url_push;
            }

            if (supportHistory && this.pushStateEnabled && options.history !== false && url_push) {
                // don't trigger 'popstate' events
                history.popstate = false;
                // if only pageIn, use 'replaceState'
                history[pageOut? "pushState": "replaceState"](null, document.title, url_push.replace(/^#/, "#&"));
            }
            // reset to popable state
            history.popstate = true;

            // store page-id, just once
            if (!store[pageid]) {
                store[pageid] = pageInto;
            }
            
            // do callback every time
            var callback = params_in.callback;
            
            if (typeof callback == "string") callback = params_in.root[callback];
            if (typeof callback == "function") callback.call(params_in.root, pageInto, pageOut, options);
        }
    };
    
    
    /**
     * For getting whole ajax url
     * In most cases, you are unnecessary to use this function
     
     * @params  trigger: dom-object. element with tag-"a".  - Optional(at least one)
                url:     string. ajax url.                  - Optional(at least one)
                params:  string|object. ajax params.        - Optional
     * @returns string
     * @example Mobilebone.getCleanUrl(elementOfA);
                Mobilebone.getCleanUrl(elementOfA, '', "a=1&b=2");
                Mobilebone.getCleanUrl(null, "xxx.html");
                Mobilebone.getCleanUrl(null, "xxx.html?a=1&b=2");
                Mobilebone.getCleanUrl(null, "xxx.html", "a=1&b=2");
    **/
    Mobilebone.getCleanUrl = function(trigger, url, params) {
        var href = "", formdata = "", clean_url = "";
        if (trigger) {
             if (trigger.nodeType == 1) {
                 // form element
                 if (trigger.action) return trigger.getAttribute("action");
                // a element
                href = trigger.getAttribute("href");
                formdata = trigger.getAttribute("data-formdata") || trigger.getAttribute("data-data");
             } else if (trigger.url) {
                 href = trigger.url;
                 formdata = trigger.data;
             }
        }

        if (!(href = href || url)) return '';
        
        // get formdata
        formdata = formdata || params || "";
        
        if (typeof formdata == "object") {
            var arr_data = [];
            for (key in params) {
                arr_data.push(key + "=" + encodeURIComponent(params[key])); 
            }
            if (arr_data.length > 0) {
                formdata = arr_data.join("&");
            } else {
                formdata = "";
            }
        }
        
        // get url of root
        clean_url = href.split("#")[0].replace(/&+$/, "");

        if (clean_url.slice(-1) == "?") {
            clean_url = clean_url.split("?")[0];    
        }
        // url = root_url + joiner + formdata
        if (formdata != "") {                       
            if (/\?/.test(clean_url)) {
                formdata = formdata.replace(/^&|\?/, "");
                clean_url = clean_url + "&" + formdata;
            } else if (formdata != "") {
                formdata = formdata.replace("?", "");
                clean_url = clean_url + "?" + formdata;
            }
        }
        
        return clean_url;
    };
    
    /**
     * Create page according to given Dom-element or HTML string. And, notice!!!!! will do transition auto.
     
     * @params  dom_or_html:        dom-object|string. Create this to dom element as a role of into-page.               - Necessary
                element_or_options: dom-object|object. '.page element', or 'a element', or 'options' for get out-page   - Optional
                options:            object.            basically, options = ajax options, of course, u can custom it!   - Optional
     * @returns undefined
     * @example Mobilebone.createPage(pageDom);
                Mobilebone.createPage(generalDom);
                Mobilebone.createPage('<div class="page out">xxx</div>');
                Mobilebone.createPage('<p>xxx</p>');
                Mobilebone.createPage(pageDom, triggerLink);
                Mobilebone.createPage(pageDom, { response: '<div...>' });
                Mobilebone.createPage(pageDom, triggerLink, { response: '<div...>' });
     *
    **/
    Mobilebone.createPage = function(dom_or_html, element_or_options, options) {
        var response = null, container = null, classPage = this.classPage;
        // 'element_or_options' can '.page element', or 'a element', or 'options'
        // basically, options = ajax options, of course, u can custom it!       
        if (!dom_or_html) return;
        options = options || {};
        
        // 'options' that 'Mobilebone.transition()' needs
        var optionsTransition = {};
        
        // get page-title from element_or_options or options
        var page_title, id_container, classPageInside;
        if (element_or_options) {
            if (element_or_options.nodeType == 1) {
                // legal elements
                if (element_or_options.href || element_or_options.action) {
                    page_title = element_or_options.getAttribute("data-title") || options.title;
                }
                response = options.response;
                id_container = element_or_options.getAttribute("data-container");
                container = document.getElementById(id_container);
                classPageInside = element_or_options.getAttribute("data-classpage");
                // pass element as target params, add on v2.3.0
                optionsTransition.target = element_or_options;
            } else {
                response = element_or_options.response || options.response; 
                page_title = element_or_options.title || options.title;
                container = element_or_options.container || options.container;
                classPageInside = element_or_options.classPage || options.classPage;
                optionsTransition.target = element_or_options.target;
            }
            if (container && classPageInside) classPage = classPageInside;  
        }
        
        // get current page(will be out) according to 'page_or_child'
        var current_page = (classPage == classPageInside? container : document).querySelector(".in." + classPage);

        // get create page (will be into) according to 'dom_or_html'
        var create_page = null;
        
        var create = document.createElement("div");
        if (typeof dom_or_html == "string") {
            create.innerHTML = dom_or_html;
        } else {
            create.appendChild(dom_or_html);
        }
        var create_title = create.getElementsByTagName("title")[0];
        // get the page element
        if (!(create_page = create.querySelector("." + classPage))) {
            create.className = classPage + " out";
            if (typeof page_title == "string") create.setAttribute("data-title", page_title);
            create_page = create;
        } else {
            if (create_title) {
                create_page.setAttribute("data-title", create_title.innerText);
            } else if (typeof page_title == "string") {
                create_page.setAttribute("data-title", page_title);
            }
        }
        // insert create page as a last-child
        (container || document.body).appendChild(create_page);
        
        // release memory
        create = null;
        
        // do transition
        optionsTransition.response = response || dom_or_html;
        optionsTransition.id = this.getCleanUrl(element_or_options) || create_page.id || ("unique" + Date.now());   
        // 'if' statement below added on v2.0.0
        if (typeof options == "object") { 
            if (typeof options.history != "undefined") {
                optionsTransition.history = options.history;
            }
            if (typeof options.remove != "undefined") {
                optionsTransition.remove = options.remove;
            }
            if (typeof options.target != "undefined") {
                optionsTransition.target = options.target;
            }
        }
        if (classPage == classPageInside) {
            optionsTransition.history = false;
            optionsTransition.classPage = classPage;
        }
        // do transition
        this.transition(create_page, current_page, optionsTransition);
    };
    
    /**
     * For ajax callback. 
     * For example, data-success="a.b.c". We can't use 'a.b.c' as a function, because it's a string. We should do some work to get it!
     
     * @params  keys:        string. - Necessary
     * @returns function
                undefined keys is not string
                window    keys undefined
     * @example Mobilebone.getFunction("a.b.c");
     *
    **/
    Mobilebone.getFunction = function(keys) {
        if (typeof keys != "string") return;
        // eg. 'globalObject.functionName'
        var fun = root, arr_key = keys.split(".");
        for (var index=0; index<arr_key.length; index+=1) {
            if (!(fun = fun[arr_key[index]])) {
                break;
            }
        }
        return fun;
    };
        
    /**
     * For ajax request to get HTML or JSON. 
     
     * @params  trigger_or_options        - Necessary  
                1. dom-object:<a>|<form>.
                2. object.  
     * @returns undefined
     * @example Mobilebone.ajax(document.querySelector("a"));
                Mobilebone.ajax({
                  url: 'xxx.html',
                  success: function() {}
                });
     *
    **/
    Mobilebone.ajax = function(trigger_or_options) {
        if (!trigger_or_options) return;
        
        // default params
        var defaults = {
            url: "",
            type: "",
            dataType: "",
            data: {},
            timeout: 10000,
            async: true,
            username: "",
            password: "",
            success: function() {},
            error: function() {},
            complete: function() {} 
        };
        
        var params = {}, ele_mask = null, formData = null;
        
        // if 'trigger_or_options' is a element, we should turn it to options-object
        var params_from_trigger = {}, attr_mask;
        if (trigger_or_options.nodeType == 1) {
            params_from_trigger = _queryToObject(trigger_or_options.getAttribute("data-params") || "");
            // get params
            for (key in defaults) {
                // data-* > data-params > defaults
                params[key] = trigger_or_options.getAttribute("data-" + key) || params_from_trigger[key] || defaults[key];
                if (typeof defaults[key] == "function" && typeof params[key] == "string") {
                    // eg. globalObject.functionName
                    params[key] = this.getFunction(params[key]);
                    if (typeof params[key] != "function") {
                        params[key] = defaults[key];
                    }
                }
            }

            params.url = this.getCleanUrl(trigger_or_options, params.url);  
            params.target = trigger_or_options;
            
            var tagName = trigger_or_options.tagName.toLowerCase();
            if (tagName == "form") {
                params.type = trigger_or_options.method;
                
                formData = new FormData(trigger_or_options);
            }   
            
            // get mask element
            attr_mask = trigger_or_options.getAttribute("data-mask");
            if (attr_mask == "true" || attr_mask == "") {
                ele_mask = trigger_or_options.querySelector("." + this.classMask);
            }
        }
        // if 'trigger_or_options' is a object
        else if (trigger_or_options.url) {
            // get params
            for (key2 in defaults) {
                params[key2] = trigger_or_options[key2] || defaults[key2];
            }
            // get url
            params.url = this.getCleanUrl(null, params.url, params.data);
            // here params.title will become page title;
            params.title = trigger_or_options.title;
        } else {
            return; 
        }
        
        // do ajax
        // get mask and loading element
        if (typeof attr_mask != "string") {
            ele_mask = document.querySelector("body > ." + this.classMask);
        }
        if (ele_mask == null) {
            ele_mask = document.createElement("div");
            ele_mask.className = this.classMask;
            ele_mask.innerHTML = '<i class="loading"></i>';
            if (typeof attr_mask == "string") {
                trigger_or_options.appendChild(ele_mask);
            } else {
                document.body.appendChild(ele_mask);
            }
        }
        // show loading
        ele_mask.style.visibility = "visible";
        
        // ajax request
        var xhr = new XMLHttpRequest();     
        xhr.open(params.type || "GET", params.url + (/\?/.test(params.url)? "&" : "?") + "r=" + Date.now(), params.async, params.username, params.password);
        xhr.timeout = params.timeout;
        
        xhr.onload = function() {
            // so far, many browser hasn't supported responseType = 'json', so, use JSON.parse instead
            var response = null;
            
            if (xhr.status == 200) {
                if (params.dataType == "json" || params.dataType == "JSON") {
                    try {
                        response = JSON.parse(xhr.response);
                        params.response = response;
                        Mobilebone.createPage(Mobilebone.jsonHandle(response), trigger_or_options, params);
                    } catch (e) {
                        params.message = "JSON parse error：" + e.message;
                        params.error.call(params, xhr, xhr.status);
                    }
                } else if (params.dataType == "unknown") {
                    // ajax send by url
                    // no history hush
                    // no element remove
                    params.history = false;
                    params.remove = false;
                    try {
                        // as json
                        response = JSON.parse(xhr.response);
                        params.response = response;
                        Mobilebone.createPage(Mobilebone.jsonHandle(response), trigger_or_options, params);
                    } catch (e) {
                        // as html
                        response = xhr.response;
                        Mobilebone.createPage(response, trigger_or_options, params);
                    }
                } else {
                    response = xhr.response;
                    // 'response' is string
                    Mobilebone.createPage(response, trigger_or_options, params);
                }
                params.success.call(params, response, xhr.status);
            } else {
                params.message = "The status code exception!";
                params.error.call(params, xhr, xhr.status);
            }
            
            params.complete.call(params, xhr, xhr.status);
            
            // hide loading
            ele_mask.style.visibility = "hidden";
        }
        
        xhr.onerror = function(e) {
            params.message = "Illegal request address or an unexpected network error!";
            params.error.call(params, xhr, xhr.status);
            // hide loading
            ele_mask.style.visibility = "hidden";
        }
        
        xhr.ontimeout = function() {
            params.message = "The request timeout!";
            params.error.call(params, xhr, xhr.status);
            // hide loading
            ele_mask.style.visibility = "hidden";
        };
        
        // set request header for server
        xhr.setRequestHeader("Type", "ajax");
        xhr.setRequestHeader("From", "mobilebone");
        
        xhr.send(formData);
    };
    
    /**
     * capture form submit events to a ajax request.
     
     * @params  form:        formElement. - Necessary
     * @example Mobilebone.form(document.querySelector("form"));
     *
    **/
    Mobilebone.submit = function(form) {
        if (!form || typeof form.action != "string") return; 
        var ajax = form.getAttribute("data-ajax");
        if (ajax == "false" || (this.captureForm == false && ajax != "true")) return;
        
        form.addEventListener("submit", function(event) {
            // prevent detect
            var attrPrevent = this.getAttribute("data-preventdefault");
            // get 'preventDefault' function
            var funPrevent = Mobilebone.getFunction(attrPrevent);
            if (typeof funPrevent == "function" && funPrevent(this) == true) {
                // if the return value of prevent function is true, prevent everything~
                event.preventDefault();
                return false;
            }
            
            Mobilebone.ajax(this);
            event.preventDefault();
        });
    };
    
    
    /**
     * Sometime we don't know direction of transition. Such as browser history change, or data-rel="auto".
       In this case, we ensure the direction(back or prev) by the sorts of two pages(into or out)
     
     * @params  page_in  dom-object      - Necessary  
                page_out  dom-object      - Optional 
                
     * @returns boolean
     *
    **/
    Mobilebone.isBack = function(page_in, page_out) {
        // back or forword, according to the order of two pages
        if (history.tempBack == true) {
            // backwords
            history.tempBack = null;
            return true;
        }
        return page_in.compareDocumentPosition(page_out) == 4;
    };
    
    /**
     * If dataType of ajax is 'json', we can't convert json-data to page-element. 
       So, we export a function names 'jsonHandle' to handle json-data.
     * Attention, it's a global interface. If your project has many json call, you should use JSON itself to make a distinction.
       For example, every JSON include the only json-id:
       {
          "id": "homePage" ,
          "data": []  
       }
       different with
       {
          "id": "listPage" ,
          "data": []  
       }
     *
     * @params  json    - Necessary         
     * @returns dom-object|string
     *
    **/
    Mobilebone.jsonHandle = function(json) {
        return '<p style="text-align:center;">Dear master, if you see me, show that JSON parsing function is undefined!</p>';
    },
    
    /**
     * Initialization. Load page according to location.hash. And bind link-catch events.
    **/
    Mobilebone.init = function() {  
        if (hasInited == true) return 'Don\'t repeat initialization!';

        var hash = location.hash.replace("#&", "#"), ele_in = null;
        if (hash == "" || hash == "#") {
            this.transition(document.querySelector("." + this.classPage));
        } else if (isSimple.test(hash) == true && (ele_in = document.querySelector(hash)) && ele_in.classList.contains(this.classPage)) { // 'ele_in' must be a page element
            this.transition(ele_in);    
        } else {
            // as a ajax
            this.ajax({
                url: hash.replace("#", ""),
                dataType: "unknown",
                error: function() {
                    ele_in = document.querySelector("." + Mobilebone.classPage);    
                    Mobilebone.transition(ele_in);
                }
            }); 
        }
        
        // Initialization link-catch events.
        var eventName = "click", $ = root.$ || root.jQuery || root.Zepto;
        if ($ && $.fn && $.fn.tap && ('ontouchstart' in window == true)) eventName = "tap"; 
    
        if (this.captureLink == true) {
            document.addEventListener(eventName, this.handleTapEvent);  
            if (eventName == "tap") {
                // zepto tap event.preventDefault can't prevent default click-events
                document.addEventListener("click", function(event) {
                    var target = event.target;
                    if (!target) return;
                    if (target.tagName.toLowerCase() != "a" && !(target = target.getParentElementByTag("a"))) {
                        return;
                    }
                    var ajax = target.getAttribute("data-ajax"), href = target.href;
                    // if not ajax request
                    if (target.getAttribute("data-rel") == "external" 
                        || ajax == "false"
                        || (href.replace("://", "").split("/")[0] !== location.href.replace("://", "").split("/")[0] && ajax != "true")
                        || (Mobilebone.captureLink == false && ajax != "true")
                    ) return;
                    event.preventDefault();
                });     
            }
        }
                
        // change flag-var for avoiding repeat init
        hasInited = true;
    };
    
    /**
     * If 'a' element has href, slide auto when tapping~
    **/
    Mobilebone.handleTapEvent = function(event) {
        /**
        // iscroll(set tap: true) may cause twice tap problem 
        // which is none of Mobilebone's business
        // However, you can let code below go to avoid twice tap in Mobilebone
        // but the tap event bind out of Mobilebone also has bug
        // so my advice is that: 
        // 1. use Date.now to judge as Mobilebone did; 
        // or
        // 2. keep this code in the form of comment and fixed bug outside
        if (store.timerTap && Date.now() - store.timerTap < 100) {  
            event.preventDefault();
            return false;
        }
        store.timerTap = Date.now();
        */

        // get target and href
        var target = event.target || event.touches[0], href = target.href;
        if ((!href || /a/i.test(target.tagName) == false) && (target = target.getParentElementByTag("a"))) {
            href = target.href;
        }

        // the page that current touched or actived
        var self_page = document.querySelector(".in." + Mobilebone.classPage);
        
        if (self_page == null || !target) return;
        
        // optional params for Mobilebone.transition
        var options = {
            target: target  
        };
        
        // prevent detect
        var attrPrevent = target.getAttribute("data-preventdefault") 
            || _queryToObject(target.getAttribute("data-params") || "").preventdefault;
        // get 'preventDefault' function
        var funPrevent = Mobilebone.getFunction(attrPrevent);
        if (typeof funPrevent == "function" && funPrevent(target) == true) {
            // if the return value of prevent function is true, prevent everything~
            event.preventDefault();
            return false;
        }
        
        // if mask element exist and displaying, prevent double trigger
        var ele_mask = target.getElementsByClassName(Mobilebone.classMask)[0];
        if (ele_mask && ele_mask.style.visibility != "hidden") {
            event.preventDefault();
            return false;
        }
        
        var idContainer = target.getAttribute("data-container"),
            classPageInside = target.getAttribute("data-classpage"),
            container = idContainer && document.getElementById(idContainer);
        if (container && classPageInside && classPageInside != Mobilebone.classPage) {
            self_page = container.querySelector(".in." + classPageInside) || container.querySelector(classPageInside);
            if (self_page == null) return false;
            options.history = false;
            options.classPage = classPageInside;
        }
        
        // if captureLink
        var capture = (Mobilebone.captureLink == true);
        // get rel
        var rel = target.getAttribute("data-rel");
        // if back
        var back = false;
        if (rel == "back") {
            back = true;
        }
                
        // if external link
        var external = (rel == "external");
        
        // if the 'href' is not legal, return
        // include:
        // 1. undefined
        // 2. javascript: (except data-rel="back")
        // 3. cros, or not capture (except data-ajax="true")
        if (!href) return;
        
        if (target.getAttribute("href").replace(/#/g, "") === "") {
            event.preventDefault();
            return;
        }

        if (/^javascript/.test(href)) {
            if (back == false) return;  
        } else {
            external = external || (href.replace("://", "").split("/")[0] !== location.href.replace("://", "").split("/")[0]);
            if ((external == true || capture == false) && target.getAttribute("data-ajax") != "true") return;
        }
        
        // judge that if it's a ajax request
        if (/^#/.test(target.getAttribute("href")) == true) {
            // hash slide
            var idTargetPage = href.split("#")[1], eleTargetPage = idTargetPage && document.getElementById(idTargetPage);
            if (back == false && rel == "auto") {
                back = Mobilebone.isBack(eleTargetPage, self_page);
            }
            if (eleTargetPage) Mobilebone.transition(eleTargetPage, self_page, back, options);
            event.preventDefault();
        } else if (/^javascript/.test(href)) {
            // back
            history.tempBack = true;
            history.back();
        } else if (target.getAttribute("data-ajax") != "false") {               
            // get a clean ajax url as page id
            var clean_url = Mobilebone.getCleanUrl(target);
            
            // if has loaded and the value of 'data-reload' is not 'true'
            var attr_reload = target.getAttribute("data-reload"), id = target.getAttribute("href");
            if ((attr_reload == null || attr_reload == "false") && store[clean_url]) {
                if (back == false && rel == "auto") {
                    back = Mobilebone.isBack(store[clean_url], self_page);
                }
                options.id = clean_url;
                Mobilebone.transition(store[clean_url], self_page, back, options);
            } else {
                Mobilebone.ajax(target);
            }
            event.preventDefault(); 
        }
    };
    
    
    /**
     * prototype extend method: get parent element by tagName
    **/
    Element.prototype.getParentElementByTag = function(tag) {
        if (!tag) return null;
        var element = null, parent = this;
        var popup = function() {
            parent = parent.parentElement;
            if (!parent) return null;
            var tagParent = parent.tagName.toLowerCase();
            if (tagParent === tag) {
                element = parent;
            } else if (tagParent == "body") {
                element = null;
            } else {
                popup();
            }
        };
        popup();
        return element;
    };
    
    /**
     * private method: convert query string to key-value object
    **/
    var _queryToObject = function(string) {
        var obj = {};
        if (typeof string == "string") {
            string.split("&").forEach(function(part) {
                var arr_part = part.split("=");
                if (arr_part.length > 1) {
                    obj[arr_part[0]] = part.replace(arr_part[0] + "=", "");
                }
            });
        }
        return obj;
    };
    
    /**
     * auto init
    **/
    window.addEventListener("DOMContentLoaded", function() {
        if (hasInited == false) {
            Mobilebone.init();
        }
    });
    
    /**
     * page change when history change
    **/
    window.addEventListener("popstate", function() {
        console.log(history.popstate);
        if (history.popstate == false) {
            history.popstate = true;
            return;
        }
        
        var hash = location.hash.replace("#&", "").replace("#", ""), page_in = null;
        if (hash == "") {
            // if no hash, get first page as 'page_in'
            page_in = document.querySelector("." + Mobilebone.classPage);
            if (page_in.id) return;         
        } else {
            page_in = store[hash];
        }
        
        if (!page_in) {
            if(isSimple.test(hash) == false) {
                // as a url
                Mobilebone.ajax({
                    url: hash,
                    dataType: "unknown"
                }); 
                return;
            }
            page_in =  document.querySelector("#" + hash)
        }
        
        var page_out = document.querySelector(".in." + Mobilebone.classPage);
        
        if ((page_in && page_in == page_out) || Mobilebone.pushStateEnabled == false) return;

        // hash ↔ id                                                    
        if (page_in) {
            Mobilebone.transition(page_in, page_out, Mobilebone.isBack(page_in, page_out), {
                history: false,
                remove: false
            });
        }
    });
    
    return Mobilebone;
});